package com.demo.controller;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import java.util.UUID;
import java.util.concurrent.TimeUnit;

/**
 * @author gy
 * @date 2022/6/18
 */
//@RestController
public class IndexController {

    private static final Logger logger = LoggerFactory.getLogger(IndexController.class);

    @Autowired
    private StringRedisTemplate stringRedisTemplate;

//    @RequestMapping("/deduct")
    public String deductStock() {

        String lockKey = "product_001";

        // 给该线程加唯一标识
        // uuid 重复的概率，完全可以忽略不记
        String clientId = UUID.randomUUID().toString();

        try {
            /**
             * redis 的 分布式锁，通过把 redis的key当锁
              */
//            Boolean result = stringRedisTemplate.opsForValue().setIfAbsent(lockKey, "stock");// jedis.setnx(key,value)
            // 如果 加 这把锁不能成功，说明已经有其他线程在使用这把锁，该线程只能等待，被阻塞；
            // 在 业务代码中，可以给出温馨提示

            /**
             * 设置过期时间解决 redis宕机，key永久失效的问题
             * 细节：和上一行代码分开写，违背了原子性操作，若在两行代码之间出问题，程序照样 GG
             */
//            stringRedisTemplate.expire(lockKey,10, TimeUnit.SECONDS);

//             一定要注意原子性操作
            Boolean result = stringRedisTemplate.opsForValue().
                    setIfAbsent(lockKey, clientId,1,TimeUnit.SECONDS);// jedis.setnx(key,value)

            if(!result){
                String client = stringRedisTemplate.opsForValue().get(lockKey);
                logger.info( "另一个实例:" + client +" 的业务还未处理完，本实例暂时无法进行业务操作");
                return "error";
            }

//            ########################### redis续命开始 （不推荐这么写，容易出bug）############################
//            Runnable runnable = new Runnable() {
//                @Override
//                public void run() {
//                    while (true) {
//                        // ------- code for task to run
//                        // ------- 要运行的任务代码
//                        if(clientId.equals(stringRedisTemplate.opsForValue().get(lockKey))) {
//                            // 如果是自己的 锁，才进行续命
//                            stringRedisTemplate.expire(lockKey, 2, TimeUnit.SECONDS);
//                        }
//                        // ------- ends here
//                        try {
//                            // sleep()：同步延迟数据，并且会阻塞线程
//                            /**
//                             * 每隔 一段时间，程序会给redis 重新赋值，实现了 redis的续命
//                             */
//                            Thread.sleep( TimeUnit.SECONDS.toMillis(2) / 3);
//                        } catch (InterruptedException e) {
//                            e.printStackTrace();
//                        }
//                    }
//                }
//            };
//
//            new Thread(runnable).start();

//            ########################### redis续命结束 （不推荐这么写，容易出bug）############################

            /**
             * 单体部署的加锁方式
             */
            //说明：关键代码，需要有原子性，需要已原子块的方式执行，不然高并发场景下会出现问题，出现库存超卖
            synchronized (this) {
                int stock = Integer.valueOf(stringRedisTemplate.opsForValue().get("stock")); // jedis.get("stock")
                if (stock > 0){
                    int realStock = stock - 1;
                    stringRedisTemplate.opsForValue().set("stock",realStock + ""); // jedis.set(key,value)

                    // 模拟业务执行时间 1s
                    Thread.sleep(1000);

                    logger.info("扣减成功，剩余库存：" + realStock + "");
                } else {
                    logger.info("扣减失败，库存不足");
                }
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            // 一定要释放锁，这把锁就会在 redis永久存放，影响其他应用实例的业务处理
            /**
             * 1、即便释放锁，会由于程序宕机、redis宕机，等问题导致，锁失效--》给key加过期时间
             *
             * 2、即便加了过期时间，仍然可能失效
             * 背景：线程1 锁过期时间到了 ，失效了，但是业务处理还没完成
             * （过期时间10秒，业务处理15秒）
             * 线程2 拿到了这把锁
             * （过期时间10秒，业务处理需要14秒）
             *
             * 线程2 当业务处理到 5秒的时候 ，线程1 程序把 线程2拿到的锁，删除了 ；以此类推，最终锁失效
             *
             * 解决方案举例：此时，如果 业务处理时间 > 锁失效时间
             * redis-server 会主动将 这把锁 释放掉
             * 假设 本线程 （product_001,123) ，业务到这还没处理完，已经花了12s，在10s的时候 redis-server把它干掉了
             * 在 11s 的时候 其他实例 拿到 product_001这把锁，赋值为 127 （product_001,127)
             * 当程序执行到 此处的时候 该key 的 value值 和 本线程不一样，就删不了 其他实例创建的 锁了
             */
            if (clientId.equals(stringRedisTemplate.opsForValue().get(lockKey))) {
                stringRedisTemplate.delete(lockKey);
            }
        }

        return "end";
    }


}
